// Package network provides network optimization features for on-demand loading
package network

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
	"time"

	"github.com/smart-snapshotter/internal/logging"
)

// DownloadManager manages parallel downloads with resume support and bandwidth control
type DownloadManager struct {
	config          *DownloadManagerConfig
	client          *http.Client
	activeDownloads sync.Map // map[string]*Download
	bandwidthLimiter *BandwidthLimiter
	queue           *DownloadQueue
	stats           *DownloadStats
	logger          logging.Logger
	wg              sync.WaitGroup
	ctx             context.Context
	cancel          context.CancelFunc
}

// DownloadManagerConfig contains download manager configuration
type DownloadManagerConfig struct {
	MaxParallelDownloads int
	ChunkSize           int64
	MaxRetries          int
	RetryDelay          time.Duration
	BandwidthLimit      int64 // bytes per second, 0 means unlimited
	UserAgent           string
	Timeout             time.Duration
	ResumeEnabled       bool
	VerifyChecksum      bool
	TempDir             string
}

// Download represents an active download
type Download struct {
	ID           string
	URL          string
	FilePath     string
	TempPath     string
	Size         int64
	Downloaded   int64
	Chunks       []*Chunk
	Status       string // "pending", "downloading", "completed", "failed", "paused"
	Error        error
	StartTime    time.Time
	EndTime      time.Time
	ResumeSupport bool
	Checksum     string
	mu           sync.RWMutex
	ctx          context.Context
	cancel       context.CancelFunc
}

// Chunk represents a download chunk
type Chunk struct {
	ID         int
	Start      int64
	End        int64
	Downloaded int64
	Status     string // "pending", "downloading", "completed", "failed"
	Error      error
	Retries    int
}

// DownloadStats contains download statistics
type DownloadStats struct {
	TotalDownloads    int64
	ActiveDownloads   int32
	CompletedDownloads int64
	FailedDownloads   int64
	TotalBytes        int64
	DownloadedBytes   int64
	AverageSpeed      int64
	StartTime         time.Time
	mu                sync.RWMutex
}

// DownloadProgress represents download progress
type DownloadProgress struct {
	ID           string
	URL          string
	Status       string
	BytesTotal   int64
	BytesDownloaded int64
	Speed        int64
	ETA          time.Duration
	Error        error
}

// NewDownloadManager creates a new download manager
func NewDownloadManager(config *DownloadManagerConfig, logger logging.Logger) (*DownloadManager, error) {
	if config.MaxParallelDownloads <= 0 {
		config.MaxParallelDownloads = 5
	}
	if config.ChunkSize <= 0 {
		config.ChunkSize = 1024 * 1024 // 1MB chunks
	}
	if config.MaxRetries <= 0 {
		config.MaxRetries = 3
	}
	if config.RetryDelay <= 0 {
		config.RetryDelay = time.Second
	}
	if config.Timeout <= 0 {
		config.Timeout = 30 * time.Minute
	}
	if config.TempDir == "" {
		config.TempDir = "/tmp/smart-snapshotter/downloads"
	}

	ctx, cancel := context.WithCancel(context.Background())

	dm := &DownloadManager{
		config:  config,
		client: &http.Client{
			Timeout: config.Timeout,
		},
		bandwidthLimiter: NewBandwidthLimiter(config.BandwidthLimit),
		queue:           NewDownloadQueue(),
		stats:           &DownloadStats{StartTime: time.Now()},
		logger:          logger,
		ctx:             ctx,
		cancel:          cancel,
	}

	// Create temp directory
	if err := os.MkdirAll(config.TempDir, 0755); err != nil {
		cancel()
		return nil, fmt.Errorf("failed to create temp directory: %w", err)
	}

	return dm, nil
}

// Start starts the download manager
func (dm *DownloadManager) Start() error {
	dm.logger.Info("Starting download manager")

	// Start download workers
	for i := 0; i < dm.config.MaxParallelDownloads; i++ {
		dm.wg.Add(1)
		go dm.downloadWorker(i)
	}

	return nil
}

// Stop stops the download manager
func (dm *DownloadManager) Stop() error {
	dm.logger.Info("Stopping download manager")
	dm.cancel()
	dm.wg.Wait()
	return nil
}

// Download starts a new download
func (dm *DownloadManager) Download(ctx context.Context, url, filePath string, checksum string) (*Download, error) {
	downloadID := generateDownloadID(url)

	// Check if already downloading
	if existing, loaded := dm.activeDownloads.LoadOrStore(downloadID, nil); loaded && existing != nil {
		if download, ok := existing.(*Download); ok {
			return download, nil
		}
	}

	// Create new download
	download := &Download{
		ID:       downloadID,
		URL:      url,
		FilePath: filePath,
		TempPath: filepath.Join(dm.config.TempDir, downloadID+".tmp"),
		Status:   "pending",
		Checksum: checksum,
		StartTime: time.Now(),
	}

	download.ctx, download.cancel = context.WithCancel(ctx)
	dm.activeDownloads.Store(downloadID, download)

	// Add to queue
	dm.queue.Add(download)

	dm.updateStats(func(stats *DownloadStats) {
		stats.TotalDownloads++
	})

	return download, nil
}

// GetDownload returns download by ID
func (dm *DownloadManager) GetDownload(downloadID string) (*Download, bool) {
	if val, ok := dm.activeDownloads.Load(downloadID); ok {
		if download, ok := val.(*Download); ok {
			return download, true
		}
	}
	return nil, false
}

// GetProgress returns download progress
func (dm *DownloadManager) GetProgress(downloadID string) (*DownloadProgress, error) {
	download, ok := dm.GetDownload(downloadID)
	if !ok {
		return nil, fmt.Errorf("download not found: %s", downloadID)
	}

	download.mu.RLock()
	defer download.mu.RUnlock()

	progress := &DownloadProgress{
		ID:              download.ID,
		URL:             download.URL,
		Status:          download.Status,
		BytesTotal:      download.Size,
		BytesDownloaded: download.Downloaded,
	}

	// Calculate speed and ETA
	elapsed := time.Since(download.StartTime)
	if elapsed > 0 && download.Downloaded > 0 {
		progress.Speed = download.Downloaded * int64(time.Second) / int64(elapsed)
		if download.Size > 0 && progress.Speed > 0 {
			remaining := download.Size - download.Downloaded
			progress.ETA = time.Duration(remaining*int64(time.Second)/progress.Speed) * time.Second
		}
	}

	return progress, nil
}

// GetStats returns download statistics
func (dm *DownloadManager) GetStats() DownloadStats {
	dm.stats.mu.RLock()
	defer dm.stats.mu.RUnlock()
	return *dm.stats
}

// downloadWorker processes downloads from the queue
func (dm *DownloadManager) downloadWorker(workerID int) {
	defer dm.wg.Done()

	dm.logger.Info("Download worker started", StringField("workerID", fmt.Sprintf("%d", workerID)))

	for {
		select {
		case <-dm.ctx.Done():
			dm.logger.Info("Download worker stopped", StringField("workerID", fmt.Sprintf("%d", workerID)))
			return
		default:
			download := dm.queue.Get()
			if download == nil {
				time.Sleep(100 * time.Millisecond)
				continue
			}

			dm.processDownload(download)
		}
	}
}

// processDownload processes a single download
func (dm *DownloadManager) processDownload(download *Download) {
	dm.updateStats(func(stats *DownloadStats) {
		stats.ActiveDownloads++
	})
	defer dm.updateStats(func(stats *DownloadStats) {
		stats.ActiveDownloads--
	})

	download.mu.Lock()
	download.Status = "downloading"
	download.mu.Unlock()

	dm.logger.Info("Starting download", StringField("downloadID", download.ID), StringField("url", download.URL))

	// Check if server supports resume
	if dm.config.ResumeEnabled {
		dm.checkResumeSupport(download)
	}

	// Perform the download
	if err := dm.performDownload(download); err != nil {
		download.mu.Lock()
		download.Status = "failed"
		download.Error = err
		download.EndTime = time.Now()
		download.mu.Unlock()

		dm.updateStats(func(stats *DownloadStats) {
			stats.FailedDownloads++
		})

		dm.logger.Error("Download failed", StringField("downloadID", download.ID), StringField("error", err.Error()))
		return
	}

	// Verify checksum if provided
	if dm.config.VerifyChecksum && download.Checksum != "" {
		if err := dm.verifyChecksum(download); err != nil {
			download.mu.Lock()
			download.Status = "failed"
			download.Error = err
			download.EndTime = time.Now()
			download.mu.Unlock()

			dm.updateStats(func(stats *DownloadStats) {
				stats.FailedDownloads++
			})

			dm.logger.Error("Checksum verification failed", StringField("downloadID", download.ID), StringField("error", err.Error()))
			return
		}
	}

	// Move temp file to final location
	if err := os.Rename(download.TempPath, download.FilePath); err != nil {
		download.mu.Lock()
		download.Status = "failed"
		download.Error = err
		download.EndTime = time.Now()
		download.mu.Unlock()

		dm.updateStats(func(stats *DownloadStats) {
			stats.FailedDownloads++
		})

		dm.logger.Error("Failed to move downloaded file", StringField("downloadID", download.ID), StringField("error", err.Error()))
		return
	}

	download.mu.Lock()
	download.Status = "completed"
	download.EndTime = time.Now()
	download.mu.Unlock()

	dm.updateStats(func(stats *DownloadStats) {
		stats.CompletedDownloads++
		stats.DownloadedBytes += download.Downloaded
	})

	dm.logger.Info("Download completed", StringField("downloadID", download.ID), StringField("bytes", fmt.Sprintf("%d", download.Downloaded)))
}

// performDownload performs the actual download
func (dm *DownloadManager) performDownload(download *Download) error {
	// For now, simple single-threaded download
	// TODO: Implement chunked parallel download

	req, err := http.NewRequestWithContext(download.ctx, "GET", download.URL, nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	if dm.config.UserAgent != "" {
		req.Header.Set("User-Agent", dm.config.UserAgent)
	}

	// Add range header if resuming
	if download.ResumeSupport && download.Downloaded > 0 {
		req.Header.Set("Range", fmt.Sprintf("bytes=%d-", download.Downloaded))
	}

	resp, err := dm.client.Do(req)
	if err != nil {
		return fmt.Errorf("failed to execute request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
		return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	// Get content length
	if resp.ContentLength > 0 {
		download.Size = resp.ContentLength
		if download.Downloaded > 0 && resp.StatusCode == http.StatusPartialContent {
			download.Size += download.Downloaded
		}
	}

	// Open temp file
	flags := os.O_CREATE | os.O_WRONLY
	if download.Downloaded > 0 {
		flags |= os.O_APPEND
	} else {
		flags |= os.O_TRUNC
	}

	file, err := os.OpenFile(download.TempPath, flags, 0644)
	if err != nil {
		return fmt.Errorf("failed to open temp file: %w", err)
	}
	defer file.Close()

	// Download with bandwidth limiting
	reader := dm.bandwidthLimiter.Reader(resp.Body)

	buffer := make([]byte, 32*1024) // 32KB buffer
	for {
		select {
		case <-download.ctx.Done():
			return download.ctx.Err()
		default:
		}

		n, err := reader.Read(buffer)
		if n > 0 {
			if _, writeErr := file.Write(buffer[:n]); writeErr != nil {
				return fmt.Errorf("failed to write to file: %w", writeErr)
			}

			atomic.AddInt64(&download.Downloaded, int64(n))
			dm.updateStats(func(stats *DownloadStats) {
				stats.TotalBytes += int64(n)
			})
		}

		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("failed to read response: %w", err)
		}
	}

	return nil
}

// checkResumeSupport checks if server supports resume
func (dm *DownloadManager) checkResumeSupport(download *Download) {
	req, err := http.NewRequestWithContext(dm.ctx, "HEAD", download.URL, nil)
	if err != nil {
		return
	}

	resp, err := dm.client.Do(req)
	if err != nil {
		return
	}
	defer resp.Body.Close()

	// Check Accept-Ranges header
	if resp.Header.Get("Accept-Ranges") == "bytes" {
		download.ResumeSupport = true
	}

	// Get content length
	if resp.ContentLength > 0 {
		download.Size = resp.ContentLength
	}
}

// verifyChecksum verifies file checksum
func (dm *DownloadManager) verifyChecksum(download *Download) error {
	file, err := os.Open(download.TempPath)
	if err != nil {
		return fmt.Errorf("failed to open file for checksum: %w", err)
	}
	defer file.Close()

	hash := sha256.New()
	if _, err := io.Copy(hash, file); err != nil {
		return fmt.Errorf("failed to calculate checksum: %w", err)
	}

	calculatedChecksum := hex.EncodeToString(hash.Sum(nil))
	if calculatedChecksum != download.Checksum {
		return fmt.Errorf("checksum mismatch: expected %s, got %s", download.Checksum, calculatedChecksum)
	}

	return nil
}

// updateStats updates download statistics
func (dm *DownloadManager) updateStats(update func(*DownloadStats)) {
	dm.stats.mu.Lock()
	defer dm.stats.mu.Unlock()
	update(dm.stats)
}

// generateDownloadID generates a unique download ID
func generateDownloadID(url string) string {
	hash := sha256.Sum256([]byte(url + time.Now().String()))
	return hex.EncodeToString(hash[:])[:16]
}